---
title_seo: Building your first API with Python and Nitric
description: Use the Nitric framework to easily build and deploy Python REST APIs for AWS, Azure or GCP
tags:
  - API
  - Key Value Store
languages:
  - python
published_at: 2022-09-11
updated_at: 2025-01-06
---

# Building your first API with Nitric

## What we'll be doing

1. Use Nitric to create an API to create and update profiles
2. Create handlers for the following API operations

| **Method** | **Route**      | **Description**                  |
| ---------- | -------------- | -------------------------------- |
| `GET`      | /profiles/     | Get all profiles                 |
| `GET`      | /profiles/[id] | Get a specific profile by its Id |
| `POST`     | /profiles      | Create a new profile             |
| `DELETE`   | /profiles/[id] | Delete a profile                 |

3. Run locally for testing
4. Deploy to a cloud of your choice
5. (Optional) Add handlers for the following API operations

| **Method** | **Route**                    | **Description**                   |
| ---------- | ---------------------------- | --------------------------------- |
| `GET`      | /profiles/[id]/image/upload  | Get a profile image upload URL    |
| `GET`      | profiles/[id]/image/download | Get a profile image download URL  |
| `GET`      | profiles/[id]/image/view     | View the image that is downloaded |

## Prerequisites

- [uv](https://docs.astral.sh/uv/#getting-started) - for simplified dependency management
- The [Nitric CLI](/get-started/installation)
- _(optional)_ Your choice of an [AWS](https://aws.amazon.com), [GCP](https://cloud.google.com) or [Azure](https://azure.microsoft.com) account

## Getting started

We'll start by creating a new project for our API.

```bash
nitric new my-profile-api py-starter
```

Next, open the project in your editor of choice.

```bash
cd my-profile-api
```

Make sure all dependencies are resolved using `uv`:

```bash
uv sync
```

The scaffolded project should have the following structure:

```text
+--.venv/
+--services/
|  +-- api.py
+--.env
+--.gitignore
+--nitric.yaml
+--.pythonversion
+--pythonproject.toml
+--uv.lock
+--README.md
```

Start the Nitric server to emulate cloud services on your machine:

```bash
nitric start
```

## Building the Profile API

This example uses UUIDs to create unique IDs to store profiles against, let's start by adding a library to help with that:

```bash
uv add uuid
```

Applications built with Nitric can contain many APIs, let's start by adding an API and a key value store to this project to serve as the public endpoint.

```python title:services/api.py
import json
from uuid import uuid4

from nitric.resources import api, kv, bucket
from nitric.application import Nitric
from nitric.context import HttpContext

# Create an api named public
profile_api = api("public")

# Access profile key value store with permissions
profiles = kv('profiles').allow('get', 'set', 'delete')

Nitric.run()
```

Here we're creating:

- An API named `public`,
- A key value store named `profiles` and giving our service permission to get and set to that store.

From here, let's add some features to that service that allow us to work with profiles.

<Note>
  You could separate some or all of these request handlers their own services if
  you prefer. For simplicity we'll group them together in this guide.
</Note>

### Create profiles with POST

```python
@profile_api.post("/profiles")
async def create_profile(ctx: HttpContext) -> None:
  pid = str(uuid4())
  name = ctx.req.json['name']
  age = ctx.req.json['age']
  hometown = ctx.req.json['homeTown']

  await profiles.set(pid, { 'name': name, 'age': age, 'hometown': hometown} )

  ctx.res.body = { 'msg': f'Profile with id {pid} created.'}
```

### Retrieve all profiles with GET

```python
@profile_api.get("/profiles")
async def get_all_profile(ctx: HttpContext) -> None:
  profile_list = []

  async for id in profiles.keys():
    d = await profiles.get(id)
    profile_list.append(d)

  ctx.res.body = json.dumps(profile_list)
  ctx.res.headers['Content-Type'] = 'application/json'
```

### Retrieve a profile with GET

```python
@profile_api.get("/profiles/:id")
async def get_profile(ctx: HttpContext) -> None:
  pid = ctx.req.params['id']
  d = await profiles.get(pid)

  ctx.res.body = json.dumps(d)
  ctx.res.headers['Content-Type'] = 'application/json'
```

### Remove a profile with DELETE

```python
@profile_api.delete("/profiles/:id")
async def delete_profiles(ctx: HttpContext) -> None:
  pid = ctx.req.params['id']

  try:
    d = await profiles.delete(pid)
    ctx.res.body = { 'msg': f'Profile with id {pid} deleted.'}
  except:
    ctx.res.status = 404
    ctx.res.body = { 'msg': f'Profile with id {pid} not found.'}
```

## Ok, let's run this thing!

Now that you have an API defined with handlers for each of its methods, it's time to test it locally.

Start the Nitric server to emulate cloud services on your machine:

```bash
nitric start
```

Once it starts, the application will receive requests via the API port. You can use cURL, Postman or any other HTTP client to test the API.

We will keep it running for our tests. If you want to update your services, just save them, they'll be reloaded automatically.

## Test your API

Update all values in brackets `[]` and change the URL to your deployed URL if you're testing on the cloud.

### Create Profile

```bash
curl --location --request POST 'http://localhost:4001/profiles' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "Peter Parker",
    "age": "21",
    "homeTown" : "Queens"
}'
```

### Fetch Profile

```bash
curl --location --request GET 'http://localhost:4001/profiles/[id]'
```

### Fetch all Profiles

```bash
curl --location --request GET 'http://localhost:4001/profiles'
```

### Delete Profile

```bash
curl --location --request DELETE 'http://localhost:4001/profiles/[id]'
```

## Deploy to the cloud

At this point, you can deploy what you've built to any of the supported cloud providers. In this example we'll deploy to AWS. Start by setting up your credentials and configuration for the [nitric/aws provider](/providers/pulumi/aws).

Next, we'll need to create a stack file (deployment target). A stack is a deployed instance of an application. You might want separate stacks for each environment, such as stacks for `dev`, `test`, and `prod`. For now, let's start by creating a file for the `dev` stack.

The `stack new` command below will create a stack named `dev` that uses the `aws` provider.

```bash
nitric stack new dev aws
```

Edit the stack file `nitric.dev.yaml` and set your preferred AWS region, for example `us-east-1`.

```yaml title:nitric.dev.yaml
provider: nitric/aws@latest
region: us-east-1
```

<Note>
  You are responsible for staying within the limits of the free tier or any
  costs associated with deployment.
</Note>

Let's try deploying the stack with the `up` command:

```bash
nitric up
```

When the deployment is complete, go to the relevant cloud console and you'll be able to see and interact with your application.

To tear down your application from the cloud, use the `down` command:

```bash
nitric down
```

## Optional - Add profile image upload/download support

If you want to go a bit deeper and create some other resources with Nitric, why not add images to your profiles API.

### Access profile buckets with permissions

Define a bucket named `profilesImg` with reading/writing permissions

```python
photos = bucket("photos").allow('read','write')
```

Add imports for time and date so that we can set up caching/expiry headers

```python
from datetime import datetime, timedelta, UTC
```

### Get a URL to upload a profile image

```python
@profile_api.get("/profiles/:id/image/upload")
async def upload_profile_image(ctx: HttpContext) -> None:
  pid = ctx.req.params['id']

  photo =  photos.file(f'images/{pid}/photo.png')
  photo_url = await photo.upload_url(expiry=timedelta(seconds=3600))

  expires = datetime.now(UTC) + timedelta(seconds=(3600))
  expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
  ctx.res.headers['Expires'] = expires

  ctx.res.body = photo_url
```

### Get a URL to download a profile image

```python
@profile_api.get("/profiles/:id/image/view")
async def download_profile_image(ctx: HttpContext) -> None:
  pid = ctx.req.params['id']

  photo =  photos.file(f'images/{pid}/photo.png')
  photo_url = await photo.download_url(expiry=timedelta(seconds=3600))

  expires = datetime.now(UTC) + timedelta(seconds=(3600))
  expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
  ctx.res.headers['Expires'] = expires

  ctx.res.body = photo_url
```

You can also directly redirect to the photo URL.

```python
@profile_api.get("/profiles/:id/image/view")
async def download_profile_image(ctx: HttpContext) -> None:
  pid = ctx.req.params['id']

  photo =  photos.file(f'images/{pid}/photo.png')
  photo_url = await photo.download_url(expiry=timedelta(seconds=3600))

  expires = datetime.now(UTC) + timedelta(seconds=(3600))
  expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
  ctx.res.headers['Expires'] = expires
  ctx.res.headers['Location'] = [photo_url]
  ctx.res.status = 303
```

#### Time to test the updated API

Update all values in brackets `[]` and change the URL to your deployed URL if you're testing on the cloud.

#### Get an image upload URL

```bash
curl --location --request GET 'http://localhost:4001/profiles/[id]/image/upload'
```

#### Using the upload URL with curl

```bash
curl --location --request PUT '[url]' \
--header 'content-type: image/png' \
--data-binary '@/home/user/Pictures/photo.png'
```

#### Get an image download URL

```bash
curl --location --request GET 'http://localhost:4001/profiles/[id]/image/download'
```
